package framework.context;

import framework.annotation.InchAutowired;
import framework.annotation.InchController;
import framework.annotation.InchService;
import framework.aop.InchAopConfig;
import framework.aop.proxy.InchAopProxy;
import framework.aop.proxy.InchCglibAopProxy;
import framework.aop.proxy.InchJdkDynamicAopProxy;
import framework.aop.support.InchAdvisedSupport;
import framework.beans.InchBeanWrapper;
import framework.beans.config.InchBeanDefinition;
import framework.beans.config.InchBeanPostProcessor;
import framework.beans.support.InchBeanDefinitionReader;
import framework.beans.support.InchDefaultListableBeanFactory;
import framework.core.InchBeanFactory;
import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Main access point, implement refresh() method in InchDefaultListableBeanFactory
 * and getBeans in InchBeanFactory
 */
@Slf4j
public class InchApplicationContext extends InchDefaultListableBeanFactory implements InchBeanFactory {

    //the location of the configuration files
    private String[] configLocations;
    //reader to read from files into InchBeanDefinition type
    private InchBeanDefinitionReader reader;

    //IoC container for Store singleton classes( why singleton bean is not wrapped?)
    private Map<String,Object> factoryBeanObjectCache = new ConcurrentHashMap<String,Object>();
    //IoC container to store [BeanName,BeanWrapper]
    private Map<String, InchBeanWrapper> factoryBeanInstanceCache = new ConcurrentHashMap<String, InchBeanWrapper>();

    /**
     * Constructor
     * @param configLocations the locations of the configuration files
     */
    public InchApplicationContext(String... configLocations) {
        this.configLocations = configLocations;
        try {
            refresh();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Get bean with a class of the bean.
     * @param beanClass the class of the bean
     * @return the bean instance(proxy)
     * @throws Exception
     */
    @Override
    public Object getBean(Class<?> beanClass) throws Exception {
        return getBean(beanClass.getName());
    }

    /**
     * The way get a bean from IoC container
     * @param beanName the name of the bean
     * @return the bean instance(proxy)
     * @throws Exception
     */
    @Override
    public Object getBean(String beanName) throws Exception {
        try {
            wrapBeanInstance(beanName);
            populateBean(beanName);
            return this.factoryBeanInstanceCache.get(beanName).getWrappedInstance();

        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * wrap bean instance into beanWrapper
     * @param beanName the name of bean that need to be wrapped
     */
    private void wrapBeanInstance(String beanName){
        if(this.factoryBeanInstanceCache.containsKey(beanName)) { return; }
        //initial a BeanPostProcessor
        InchBeanPostProcessor beanPostProcessor = new InchBeanPostProcessor();
        //get a instance using beanDefinition
        Object instance = instantiateBean(beanName);
        // check if instantiate method works well or not
        if(instance == null) {
            log.info("Can't find parameter class for:\t" + beanName);
            return;
        }
        //before initialization
        beanPostProcessor.postProcessBeforeInitialization(instance,beanName);
        //decorator model to create bean
        InchBeanWrapper beanWrapper = new InchBeanWrapper(instance);
        //put into cache
        this.factoryBeanInstanceCache.put(beanName, beanWrapper);
        //after initialization
        beanPostProcessor.postProcessAfterInitialization(instance,beanName);
    }


    /**
     * set autowired class field
     * @param beanName
     */
    private void populateBean(String beanName) {
        Object instance = factoryBeanInstanceCache.get(beanName).getOriginClass();
        Class<?> clazz = instance.getClass();
        if(!(clazz.isAnnotationPresent(InchController.class) || clazz.isAnnotationPresent(InchService.class))) {
            return;
        }
        Field[] fields = clazz.getDeclaredFields();
        for(Field field : fields) {
           if(!field.isAnnotationPresent(InchAutowired.class)) {continue;}
           InchAutowired autowired = field.getAnnotation(InchAutowired.class);
           String autoWiredBeanName = autowired.value().trim();
           if(autoWiredBeanName.equals("")) {
               autoWiredBeanName = field.getType().getName();
           }
           //check if the bean instance is initial
           if(!this.factoryBeanInstanceCache.containsKey(autoWiredBeanName)) {
               wrapBeanInstance(autoWiredBeanName);
           }
            field.setAccessible(true);
           try {
                field.set(instance,this.factoryBeanInstanceCache.get(autoWiredBeanName).getWrappedInstance());
           } catch (IllegalAccessException e) {
                e.printStackTrace();
           }
        }
    }


    /**
     * method that return a instance of a bean
     * @param beanName the bean name
     * @return singleton bean instance
     */
    private Object instantiateBean(String beanName) {
        InchBeanDefinition beanDefinition = super.beanDefinitionMap.get(beanName);
        Object instance = null;
        //get the type name of the bean
        String className = beanDefinition.getBeanClassName();
        try {
            //check if the bean instance is already in cache
            //get it if in cache, if not create new and put in cache
            if (beanDefinition.isSingleton()) {
                if(this.factoryBeanObjectCache.containsKey(className)) {
                    Class<?> clazz = Class.forName(className);
                    instance = this.beanDefinitionMap.get(className);
                    instance = getAopProxyInstance(clazz,instance);
                } else {
                    Class<?> clazz = Class.forName(className);
                    instance = clazz.getConstructor().newInstance();
                    instance = getAopProxyInstance(clazz,instance);
                    this.factoryBeanObjectCache.put(beanDefinition.getFactoryBeanName(),instance);
                }
            } else {
                Class<?> clazz = Class.forName(className);
                instance = clazz.getConstructor().newInstance();
                instance = getAopProxyInstance(clazz,instance);
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        return instance;
    }


    /**
     * get an Aop proxy instance for the current bean
     * @param targetClass the class for the bean instance
     * @param targetInstance the bean instance
     * @return an Aop proxy instance
     */
    private Object getAopProxyInstance(Class<?> targetClass, Object targetInstance) {
        Object instance = null;
        //get a advise support to know use which proxy: JDK or Cglib
        InchAdvisedSupport advisedSupport = getAdviseSupport(targetClass,targetInstance);
        //check if the class is a point cut bean before create proxy
        if(advisedSupport.pointCutMatch()){
            instance = createProxy(advisedSupport).getProxy();
        }
        return instance == null ? targetInstance : instance;
    }


    /**
     * create a advise support class of a bean
     * @param targetClass the class of the target bean
     * @param targetInstance the instance of the bean
     * @return an Advise support class
     */
    private InchAdvisedSupport getAdviseSupport(Class<?> targetClass, Object targetInstance) {
        InchAopConfig config= new InchAopConfig(reader.getConfig());
        InchAdvisedSupport advisedSupport = new InchAdvisedSupport(config);
        advisedSupport.setTargetClass(targetClass);
        advisedSupport.setTarget(targetInstance);
        advisedSupport.parse();
        return advisedSupport;
    }

    /**
     * create a proxy using an advise support
     * @param config
     * @return
     */
    private InchAopProxy createProxy(InchAdvisedSupport config) {
        Class<?> targetClass = config.getTargetClass();
        //if there is at least one interface of the bean use JDK
        if(targetClass.getInterfaces().length > 0) {
            return new InchJdkDynamicAopProxy(config);
        }
        //if no interface then use CgLib
        return new InchCglibAopProxy(config);
    }




    /**
     * main function
     * @throws Exception
     */
    @Override
    public void refresh() throws Exception {
        //initialize reader for the configuration files
        reader = new InchBeanDefinitionReader(this.configLocations);
        //load configuration files into beanDefinition format;
        List<InchBeanDefinition> beanDefinitions = reader.loadBeanDefinitions();
        //load beans information into ioc container
        doRegisterBeanDefinition(beanDefinitions);
        //pre load bean instance if needed (lazy initial is true)
        doAutowired();
    }


    /**
     * put BeanDefinition and bean name into ioc container
     * @param beanDefinitions Definitions of beans
     * @throws Exception bean already exists exception
     */
    private void doRegisterBeanDefinition(List<InchBeanDefinition> beanDefinitions) throws Exception {
        //loop and search all the beanDefinitions read from bean configuration files
        for (InchBeanDefinition beanDefinition: beanDefinitions) {
            //check duplicated
            if(super.beanDefinitionMap.containsKey(beanDefinition.getFactoryBeanName())) {
                throw new Exception("The \"" + beanDefinition.getFactoryBeanName() + "\" bean exists");
            }
            //put bean info into IoC container
            super.beanDefinitionMap.put(beanDefinition.getFactoryBeanName(), beanDefinition);
        }
    }

    /**
     * auto initial bean if needed
     */
    private void doAutowired() {
        for (Map.Entry<String,InchBeanDefinition> beanDefinitionEntry : super.beanDefinitionMap.entrySet()) {
            String beanName = beanDefinitionEntry.getKey();
            //only deal with the ones that don't need pre init
            if(!beanDefinitionEntry.getValue().isLazyInit()){
                try {
                    getBean(beanName);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * @return bean names in the bean definitions
     */
    public String[] getBeanDefinitionNames() {
        return this.beanDefinitionMap.keySet().toArray(new String[this.beanDefinitionMap.size()]);
    }

    /**
     * @return the number of bean definitions
     */
    public int getBeanDefinitionCount() {
        return this.beanDefinitionMap.size();
    }

    /**
     * @return configurations
     */
    public Properties getConfig() {
        return this.reader.getConfig();
    }
}
